package com.gitee.wsl.func.filter.bloomfilter.bean

import com.gitee.wsl.ext.bitcode.bitCount
import com.gitee.wsl.func.filter.bloomfilter.api.BitMapExtractor
import com.gitee.wsl.func.filter.bloomfilter.api.BloomFilter
import com.gitee.wsl.func.operator.LongBinaryOperator
import com.gitee.wsl.func.predicate.BiPredicate
import com.gitee.wsl.func.predicate.Predicate
import kotlin.math.sqrt


/**
 * Implementations of set operations on BitMapExtractors.
 *
 * @since 4.5.0-M1
 */
object SetOperations {
    /**
     * Calculates the cardinality of the logical `AND` of the bit maps for the two filters.
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor
     * @return the cardinality of the `AND` of the filters.
     */
    fun andCardinality(first: BitMapExtractor, second: BitMapExtractor): Int {
        return cardinality(first, second, LongBinaryOperator { x, y -> x and y })
    }

    /**
     * Calculates the cardinality of a BitMapExtractor. By necessity this method will visit each bit map created by the bitMapExtractor.
     *
     * @param bitMapExtractor the extractor to calculate the cardinality for.
     * @return the cardinality of the bit maps produced by the bitMapExtractor.
     */
    fun cardinality(bitMapExtractor: BitMapExtractor): Int {
        val cardinality = IntArray(1)
        bitMapExtractor.processBitMaps(Predicate { l: Long ->
            cardinality[0] += l.bitCount()
            true
        })
        return cardinality[0]
    }

    /**
     * Calculates the cardinality of the result of a LongBinaryOperator using the `BitMapExtractor.makePredicate` method.
     *
     * @param first  the first BitMapExtractor
     * @param second the second BitMapExtractor
     * @param op     a long binary operation on where x = `first` and y = `second` bitmap extractors.
     * @return the calculated cardinality.
     */
    private fun cardinality(first: BitMapExtractor, second: BitMapExtractor, op: LongBinaryOperator): Int {
        val cardinality = IntArray(1)

        first.processBitMapPairs(second, BiPredicate { x: Long, y: Long ->
            cardinality[0] += (op.applyAsLong(x, y)).bitCount()
            true
        })
        return cardinality[0]
    }

    /**
     * Calculates the Cosine distance between two BitMapExtractor.
     *
     *
     * Cosine distance is defined as `1 - Cosine similarity`
     *
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor.
     * @return the jaccard distance.
     */
    fun cosineDistance(first: BitMapExtractor, second: BitMapExtractor): Double {
        return 1.0 - SetOperations.cosineSimilarity(first, second)
    }

    /**
     * Calculates the Cosine similarity between two BitMapExtractors.
     *
     *
     * Also known as Orchini similarity and the Tucker coefficient of congruence or Ochiai similarity.
     *
     *
     *
     * If either extractor is empty the result is 0 (zero)
     *
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor.
     * @return the Cosine similarity.
     */
    fun cosineSimilarity(first: BitMapExtractor, second: BitMapExtractor): Double {
        val numerator = andCardinality(first, second)
        // Given that the cardinality is an int then the product as a double will not
        // overflow, we can use one sqrt:
        return if (numerator == 0) 0.0 else numerator / sqrt((cardinality(first) * cardinality(second)).toDouble())
    }

    /**
     * Calculates the Cosine similarity between two Bloom filters.
     *
     *
     * Also known as Orchini similarity and the Tucker coefficient of congruence or Ochiai similarity.
     *
     *
     *
     * If either filter is empty (no enabled bits) the result is 0 (zero)
     *
     *
     *
     * This is a version of cosineSimilarity optimized for Bloom filters.
     *
     *
     * @param first  the first Bloom filter.
     * @param second the second Bloom filter.
     * @return the Cosine similarity.
     */
    fun cosineSimilarity(first: BloomFilter<*>, second: BloomFilter<*>): Double {
        val numerator = andCardinality(first, second)
        // Given that the cardinality is an int then the product as a double will not
        // overflow, we can use one sqrt:
        return if (numerator == 0) 0.0 else (numerator / sqrt((first.cardinality() * second.cardinality()).toDouble()))
    }

    /**
     * Calculates the Hamming distance between two BitMapExtractors.
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor.
     * @return the Hamming distance.
     */
    fun hammingDistance(first: BitMapExtractor, second: BitMapExtractor): Int {
        return xorCardinality(first, second)
    }

    /**
     * Calculates the Jaccard distance between two BitMapExtractor.
     *
     *
     * Jaccard distance is defined as `1 - Jaccard similarity`
     *
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor.
     * @return the Jaccard distance.
     */
    fun jaccardDistance(first: BitMapExtractor, second: BitMapExtractor): Double {
        return 1.0 - jaccardSimilarity(first, second)
    }

    /**
     * Calculates the Jaccard similarity between two BitMapExtractor.
     *
     *
     * Also known as Jaccard index, Intersection over Union, and Jaccard similarity coefficient
     *
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor.
     * @return the Jaccard similarity.
     */
    fun jaccardSimilarity(first: BitMapExtractor, second: BitMapExtractor): Double {
        val cardinality = IntArray(2)
        first.processBitMapPairs(second, BiPredicate { x: Long?, y: Long? ->
            cardinality[0] += (x!! and y!!).bitCount()
            cardinality[1] += (x or y).bitCount()
            true
        })
        val intersection = cardinality[0]
        return if (intersection == 0) 0.0 else intersection / cardinality[1].toDouble()
    }

    /**
     * Calculates the cardinality of the logical `OR` of the bit maps for the two filters.
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor
     * @return the cardinality of the `OR` of the filters.
     */
    fun orCardinality(first: BitMapExtractor, second: BitMapExtractor): Int {
        return cardinality(first, second, LongBinaryOperator { x, y -> x or y })
    }

    /**
     * Calculates the cardinality of the logical `XOR` of the bit maps for the two filters.
     *
     * @param first  the first BitMapExtractor.
     * @param second the second BitMapExtractor
     * @return the cardinality of the `XOR` of the filters.
     */
    fun xorCardinality(first: BitMapExtractor, second: BitMapExtractor): Int {
        return cardinality(first, second, LongBinaryOperator { x, y -> x xor y })
    }
}
